/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */
import { forwardRef, useImperativeHandle, useRef, useEffect, useState } from 'react';

import {$createLinkNode} from '@lexical/link';
import {$createListItemNode, $createListNode} from '@lexical/list';
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {$createHeadingNode, $createQuoteNode} from '@lexical/rich-text';
import {
  $createParagraphNode,
  $createTextNode,
  $getRoot,
  $isTextNode,
  DOMConversionMap,
  TextNode,
  SerializedEditorState,
} from 'lexical';
import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {FlashMessageContext} from './context/FlashMessageContext';
import {SettingsContext, useSettings} from './context/SettingsContext';
import {SharedHistoryContext} from './context/SharedHistoryContext';
import {ToolbarContext} from './context/ToolbarContext';
import Editor from './Editor';
import PlaygroundNodes from './nodes/PlaygroundNodes';
import {TableContext} from './plugins/TablePlugin';
import {parseAllowedFontSize} from './plugins/ToolbarPlugin/fontSize';
import Settings from './Settings';
import PlaygroundEditorTheme from './themes/PlaygroundEditorTheme';
import {parseAllowedColor} from './ui/ColorPicker';

export interface LexicalEditorRef {
  getJSON: () => SerializedEditorState | null;
  loadJSON: (jsonData: SerializedEditorState) => void;
  getHTML: () => string;
  loadHTML: (htmlString: string) => void;
  clear: () => void;
  setPreviewMode: (isPreview: boolean) => void;
}

interface LexicalEditorProps {
  initialData?: SerializedEditorState | string;
  dataType?: 'json' | 'html';
  onDataChange?: (data: SerializedEditorState) => void;
  previewMode?: boolean;
}

function $prepopulatedRichText() {
  const root = $getRoot();
  if (root.getFirstChild() === null) {
    const heading = $createHeadingNode('h1');
    heading.append($createTextNode('Welcome to the playground'));
    root.append(heading);
    const quote = $createQuoteNode();
    quote.append(
      $createTextNode(
        `In case you were wondering what the black box at the bottom is – it's the debug view, showing the current state of the editor. ` +
          `You can disable it by pressing on the settings control in the bottom-left of your screen and toggling the debug view setting.`,
      ),
    );
    root.append(quote);
    const paragraph = $createParagraphNode();
    paragraph.append(
      $createTextNode('The playground is a demo environment built with '),
      $createTextNode('@lexical/react').toggleFormat('code'),
      $createTextNode('.'),
      $createTextNode(' Try typing in '),
      $createTextNode('some text').toggleFormat('bold'),
      $createTextNode(' with '),
      $createTextNode('different').toggleFormat('italic'),
      $createTextNode(' formats.'),
    );
    root.append(paragraph);
    const paragraph2 = $createParagraphNode();
    paragraph2.append(
      $createTextNode(
        'Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!',
      ),
    );
    root.append(paragraph2);
    const paragraph3 = $createParagraphNode();
    paragraph3.append(
      $createTextNode(`If you'd like to find out more about Lexical, you can:`),
    );
    root.append(paragraph3);
    const list = $createListNode('bullet');
    list.append(
      $createListItemNode().append(
        $createTextNode(`Visit the `),
        $createLinkNode('https://lexical.dev/').append(
          $createTextNode('Lexical website'),
        ),
        $createTextNode(` for documentation and more information.`),
      ),
      $createListItemNode().append(
        $createTextNode(`Check out the code on our `),
        $createLinkNode('https://github.com/facebook/lexical').append(
          $createTextNode('GitHub repository'),
        ),
        $createTextNode(`.`),
      ),
      $createListItemNode().append(
        $createTextNode(`Playground code can be found `),
        $createLinkNode(
          'https://github.com/facebook/lexical/tree/main/packages/lexical-playground',
        ).append($createTextNode('here')),
        $createTextNode(`.`),
      ),
      $createListItemNode().append(
        $createTextNode(`Join our `),
        $createLinkNode('https://discord.com/invite/KmG4wQnnD9').append(
          $createTextNode('Discord Server'),
        ),
        $createTextNode(` and chat with the team.`),
      ),
    );
    root.append(list);
    const paragraph4 = $createParagraphNode();
    paragraph4.append(
      $createTextNode(
        `Lastly, we're constantly adding cool new features to this playground. So make sure you check back here when you next get a chance :).`,
      ),
    );
    root.append(paragraph4);
  }
}

function getExtraStyles(element: HTMLElement): string {
  // Parse styles from pasted input, but only if they match exactly the
  // sort of styles that would be produced by exportDOM
  let extraStyles = '';
  const fontSize = parseAllowedFontSize(element.style.fontSize);
  const backgroundColor = parseAllowedColor(element.style.backgroundColor);
  const color = parseAllowedColor(element.style.color);
  if (fontSize !== '' && fontSize !== '15px') {
    extraStyles += `font-size: ${fontSize};`;
  }
  if (backgroundColor !== '' && backgroundColor !== 'rgb(255, 255, 255)') {
    extraStyles += `background-color: ${backgroundColor};`;
  }
  if (color !== '' && color !== 'rgb(0, 0, 0)') {
    extraStyles += `color: ${color};`;
  }
  return extraStyles;
}

function buildImportMap(): DOMConversionMap {
  const importMap: DOMConversionMap = {};

  // Wrap all TextNode importers with a function that also imports
  // the custom styles implemented by the playground
  for (const [tag, fn] of Object.entries(TextNode.importDOM() || {})) {
    importMap[tag] = (importNode) => {
      const importer = fn(importNode);
      if (!importer) {
        return null;
      }
      return {
        ...importer,
        conversion: (element) => {
          const output = importer.conversion(element);
          if (
            output === null ||
            output.forChild === undefined ||
            output.after !== undefined ||
            output.node !== null
          ) {
            return output;
          }
          const extraStyles = getExtraStyles(element);
          if (extraStyles) {
            const {forChild} = output;
            return {
              ...output,
              forChild: (child, parent) => {
                const textNode = forChild(child, parent);
                if ($isTextNode(textNode)) {
                  textNode.setStyle(textNode.getStyle() + extraStyles);
                }
                return textNode;
              },
            };
          }
          return output;
        },
      };
    };
  }

  return importMap;
}

function EditorContent({ initialData, dataType, onDataChange, onRefReady, previewMode }: LexicalEditorProps & { onRefReady: (ref: LexicalEditorRef) => void }) {
  const [editor] = useLexicalComposerContext();

    // Set up the ref methods
  const editorRefMethods: LexicalEditorRef = {
    getJSON: () => {
      return editor.getEditorState().toJSON();
    },
    loadJSON: (jsonData: SerializedEditorState) => {
      editor.update(() => {
        const editorState = editor.parseEditorState(jsonData);
        editor.setEditorState(editorState);
      });
    },
    getHTML: () => {
      let html = '';
      editor.getEditorState().read(() => {
        html = $generateHtmlFromNodes(editor, null);
      });
      return html;
    },
    loadHTML: (htmlString: string) => {
      editor.update(() => {
        const parser = new DOMParser();
        const dom = parser.parseFromString(htmlString, 'text/html');
        const nodes = $generateNodesFromDOM(editor, dom);
        const root = $getRoot();
        root.clear();
        root.append(...nodes);
      });
    },
    clear: () => {
      editor.update(() => {
        const root = $getRoot();
        root.clear();
      });
    },
    setPreviewMode: (isPreview: boolean) => {
      editor.setEditable(!isPreview);
    },
  };

  // Expose ref methods to parent
  useEffect(() => {
    onRefReady(editorRefMethods);
  }, []);

  // Update editor editable state when previewMode changes
  useEffect(() => {
    editor.setEditable(!previewMode);
  }, [editor, previewMode]);

  // Load initial data if provided
  useEffect(() => {
    if (initialData) {
      if (dataType === 'html' && typeof initialData === 'string') {
        editorRefMethods.loadHTML(initialData);
      } else if (dataType === 'json' && typeof initialData === 'object') {
        editorRefMethods.loadJSON(initialData);
      }
    }
  }, [initialData, dataType]);

  // Listen for editor changes and call onDataChange
  useEffect(() => {
    if (onDataChange) {
      return editor.registerUpdateListener(({ editorState }) => {
        onDataChange(editorState.toJSON());
      });
    }
  }, [editor, onDataChange]);

  return (
    <>
      <div className="editor-shell">
        <Editor previewMode={previewMode} />
      </div>
      {!previewMode && <Settings />}
    </>
  );
}

const LexicalEditor = forwardRef<LexicalEditorRef, LexicalEditorProps>(
  ({ initialData, dataType = 'json', onDataChange, previewMode = false }, ref) => {
    const [isPreviewMode, setIsPreviewMode] = useState(previewMode);
    let currentRef: LexicalEditorRef | null = null;

    useImperativeHandle(ref, () => ({
      getJSON: () => {
        return currentRef?.getJSON() || null;
      },
      loadJSON: (jsonData: SerializedEditorState) => {
        currentRef?.loadJSON(jsonData);
      },
      getHTML: () => {
        return currentRef?.getHTML() || '';
      },
      loadHTML: (htmlString: string) => {
        currentRef?.loadHTML(htmlString);
      },
      clear: () => {
        currentRef?.clear();
      },
      setPreviewMode: (isPreview: boolean) => {
        setIsPreviewMode(isPreview);
        currentRef?.setPreviewMode(isPreview);
      },
    }));

    const handleRefReady = (editorRef: LexicalEditorRef) => {
      currentRef = editorRef;
    };

    return (
      <SettingsContext>
        <FlashMessageContext>
          <LexicalEditorCore
            initialData={initialData}
            dataType={dataType}
            onDataChange={onDataChange}
            onRefReady={handleRefReady}
            previewMode={isPreviewMode}
          />
        </FlashMessageContext>
      </SettingsContext>
    );
  }
);

const LexicalEditorCore = ({ initialData, dataType, onDataChange, onRefReady, previewMode }: LexicalEditorProps & { onRefReady: (ref: LexicalEditorRef) => void }) => {
  const {
    settings: { isCollab, emptyEditor, measureTypingPerf },
  } = useSettings();

  // Determine initial editor state
  let editorState: any;
  if (initialData && dataType === 'json') {
    editorState = null; // We'll load it after initialization
  } else if (isCollab) {
    editorState = null;
  } else if (emptyEditor) {
    editorState = undefined;
  } else {
    editorState = $prepopulatedRichText;
  }

  const initialConfig = {
    editorState,
    html: { import: buildImportMap() },
    namespace: 'Playground',
    nodes: [...PlaygroundNodes],
    onError: (error: Error) => {
      throw error;
    },
    theme: PlaygroundEditorTheme,
  };

  return (
    <LexicalComposer initialConfig={initialConfig}>
      <SharedHistoryContext>
        <TableContext>
          <ToolbarContext>
            <EditorContent
              initialData={initialData}
              dataType={dataType}
              onDataChange={onDataChange}
              onRefReady={onRefReady}
              previewMode={previewMode}
            />
          </ToolbarContext>
        </TableContext>
      </SharedHistoryContext>
    </LexicalComposer>
  );
};

LexicalEditor.displayName = 'LexicalEditor';

export default LexicalEditor;